Flutter 正则匹配实战:视图篇 您所在的位置:网站首页 win10 系统选择界面一闪而过 Flutter 正则匹配实战:视图篇

Flutter 正则匹配实战:视图篇

2023-06-17 22:35| 来源: 网络整理| 查看: 265

目录

一、应用的暗黑模式

1. 暗黑模式实现的原理

2.修改自适应组件暗黑样式

3. 项目中需自己适配暗黑的组件

4. 暗黑主题数据来源

二、基于状态管理切换模式

1. 应用配置数据 AppConfig

2. 业务逻辑层

3. 视图层处理

三、配置数据的持久化存储

1. 存储与读取

2.首屏界面

3. 业务逻辑的处理

4.关于业务逻辑状态的监听

四、小细节的优化

1. 按钮的细节处理

2. 点击按钮的缩放动画

3. 文件选择的优化

4. 记录多页签处理

一、应用的暗黑模式

关于组件的暗黑模式,有如下三大类情况,需要区分对待:

1. 暗黑模式实现的原理

下面是默认情况下的计数器项目,在通过 darkTheme 之后,修改 themeMode 的值,就可以切换亮色和暗色:

lightdark

return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, // themeMode: ThemeMode.light, // 亮色 themeMode: ThemeMode.dark, // 暗色 darkTheme: ThemeData(brightness: Brightness.dark), theme: ThemeData( primarySwatch: Colors.blue,), home: const MyHomePage(title: 'Flutter Demo Home Page'), );

从这里可以看出,Flutter 的内置组件自身有响应 主题模式 变化的功能。其实从本质上来说,这种手段我们是非常常见的,就是: 跨节点的数据共享 而已。

拿 Scaffold 的背景颜色来说,当使用者未指定背景色时,会通过 context 向上查找主题数据 ThemeData,使用 scaffoldBackgroundColor 颜色来赋值:

final ThemeData themeData = Theme.of(context);

在 MaterialApp 状态类中,会根据 themeMode 来设置主体数据。如下所示,在 themeMode.dark 且 darkTheme 非空时,就是使用 darkTheme 提供的数据。这就是 暗黑模式 最基本的原理,其他的组件能够响应暗黑模式,也是如此:

2.修改自适应组件暗黑样式

这就一个问题,自适应的样式一般来说并不符合设计的着色。我们如何指定某些颜色,比如 scaffoldBackgroundColor 或 悬浮按钮颜色:

标题

如果明白上面的着色原理,其实非常简单,只有在 darkTheme 中指定相关主体数据即可。对于拥有主题样式的组件,都可以通过这种方式来提供 默认的 暗黑模式主题样式:

darkTheme: ThemeData( brightness: Brightness.dark, scaffoldBackgroundColor: const Color(0xff2B2B2B), floatingActionButtonTheme: const FloatingActionButtonThemeData( foregroundColor: Colors.white, backgroundColor: Color(0xff506D9F) ) ), 3. 项目中需自己适配暗黑的组件

从上面源码可以看出一点:组件自适应的暗黑样式优先级,要低于用户的显示指定样式。比如用户如果主动为 Scaffold 设置了背景色,主题数据是不生效的。

另外,由于并非所有组件都可以设置主题,特别是一些自定义的组件。当把颜色写死之后,暗黑模式肯定就需要额外适配。比如,现在惨不忍睹的暗黑模式:

比如现在要进行暗黑模式的适配,达到如下效果:

最直接的方式是在自己封装组件时,根据 Theme 中的 brightness,来判断是否处于暗黑模式,区分填色:

bool isDark = Theme.of(context).brightness == Brightness.dark; Color color = isDark ? const Color(0xff2C2E2F) : const Color(0xffBDBDBD);

但这样做一个问题,写起来太过复杂,而且颜色也是约等于写死,分散在各处,未来想要调整非常痛苦。理论上,最好能自定义一套 App 主题数据,仿照 Theme 的方式,进行跨节点传输样式。这样可以自己制定样式,相对灵活。

但缺点也很明显:封装的组件依赖自定义的样式数据,别人复用起来就必须引入样式。如果不是 ui 组件库,感觉没有必要自己定制一套样式。可以来一招:借花献佛。

4. 暗黑主题数据来源

一件事物的价值,并不在于它 叫什么,而在于它 做了什么 。一个盘子叫 果盘,并不代表它只能用于装水果,没有必要被 事物的名称 限制了其使用场景。如果对 ThemeData 比较熟悉的话,可以知道其中定义了非常多类型的主题数据。我们只需要为 项目里的菜 找到对应的 果盘,放进去即可:

拿导航栏来说,内置的 NavigationRailTheme 就非常适合:

在组件构建时,在对应的 果盘 中取菜即可:

navigationRailTheme: const NavigationRailThemeData( // 未选中记录标题样式 unselectedLabelTextStyle: TextStyle( color: Color(0xffBBBBBB), fontSize: 13, fontWeight: FontWeight.bold, ), // 选中记录标题样式 selectedLabelTextStyle: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold, ), // 记录面板颜色 backgroundColor: Color(0xff3C3F41), // 导航栏图标颜色 indicatorColor: Color(0xffAFB1B3), // 激活页签背景色 selectedIconTheme: IconThemeData(color: Color(0xff2C2E2F)), ),

其他颜色,尽量选用语义类似的属性。目前这些颜色配置就可以满足上面的暗黑模式的样式需求:

brightness: Brightness.dark, // 外框背景色 backgroundColor: const Color(0xff3C3F41), // 记录激活高亮色 highlightColor: const Color(0xff0D293E), // scaffold 背景颜色 scaffoldBackgroundColor: const Color(0xff2B2B2B), // 分割线颜色 dividerColor: const Color(0xff323232), // 主颜色 primaryColor: Colors.blue, // 主内容文字样式 textTheme: const TextTheme( displayMedium: TextStyle(color: Color(0xffA9B7C6), fontSize: 14), ), // 输入框填充色 inputDecorationTheme: const InputDecorationTheme( fillColor: Color(0xff45494A) ),

然后再给出一套亮色的主题数据,更改 themeMode 之后,即可得到亮色模式:

同样,其他面板也类似进行暗黑模式的适配,效果如下:

二、基于状态管理切换模式

我们现在只要切换 themeMode 的模式即可完成暗亮模式切换。根据前面的经验,很容易想到:使用状态管理实现。

1. 应用配置数据 AppConfig

像这种全局的配置信息,可以通过一个类来维护数据。不止是暗黑模式类型,未来来可以拓展 字号、字体、匹配色、语言 等应用级的数据信息,以供用户设置。

---->[models/app_config/app_config.dart]---- const Map kAppThemeMap = { 0: ThemeMode.light, 1: ThemeMode.dark, }; class AppConfig extends Equatable{ /// 关联关系见 [kAppThemeMap] final int appThemeMode; const AppConfig({this.appThemeMode = 0}); ThemeMode get themeMode=> kAppThemeMap[appThemeMode]!; AppConfig copyWith({int? appThemeMode}){ return AppConfig( appThemeMode:appThemeMode??this.appThemeMode ); } @override List get props => [appThemeMode]; } 2. 业务逻辑层

现在业务逻辑非常简单,通过 Cubit 来处理就行了。如下 switchThemeMode 方法,用于主题模式的切换:

---->[blocs/app_config/bloc.dart]---- class AppConfigBloc extends Cubit { AppConfigBloc() : super(const AppConfig()); void switchThemeMode() { int newMode = state.appThemeMode == 0 ? 1 : 0; emit(state.copyWith(appThemeMode: newMode)); } } 3. 视图层处理

接下来的事就非常简单了,在 MyApp#build 中使用 select 查询并监听 themeMode 的变化,将 ThemeMode 对象为 MaterialApp 的对应属性赋值即可:

最后看事件的触发,如下添加一个模式转换的按钮,点击时通过 context 获取 AppConfigBloc,触发其 switchThemeMode 方法转换状态。这样就能实现点击切换:

class ThemeSwitchButton extends StatelessWidget { const ThemeSwitchButton({super.key}); @override Widget build(BuildContext context) { ThemeMode mode = context.select( (value) => value.state.themeMode, ); Widget icon = mode == ThemeMode.dark ? const Icon(TolyIcon.wb_sunny, size: 22) : const Icon(TolyIcon.dark, size: 22); return Padding( padding: const EdgeInsets.only(left: 10, right: 20, top: 8.0, bottom: 8), child: GestureDetector( onTap: () => _switchTheme(context), child: icon, ), ); } void _switchTheme(BuildContext context) { context.read().switchThemeMode(); } } 三、配置数据的持久化存储

现在,退出程序之后,选择的配置就会失效。如果想要保持配置,就需要对配置进行存储,在程序运行前读取并设置。数据持久化的方式有很多,虽然可以使用数据库,但一般配置信息比较简单,可以选择 【shared_preferences】 插件,目前已经支持全平台:

1. 存储与读取

由于 shared_preferences 是键值对,我习惯上用将 key 在类中作为静态常量来维护,这样方便调用:

class SpKey{ static const String appThemeModel = 'AppThemeModelSpKey'; }

然后,主题数据的切换是在 AppConfigBloc 中处理的业务逻辑。所以在这里通过 SharedPreferences 在切换时,设置值即可。如下是是在 macos 中的记录情况:

项目包/Data/Library/Preferences 文件夹下

class AppConfigBloc extends Cubit { SharedPreferences? _sp; Future get sp async { _sp ??= await SharedPreferences.getInstance(); return _sp!; } AppConfigBloc() : super(const AppConfig()); void initConfig() async{ // 读取数据 int mode = (await sp).getInt(SpKey.appThemeModel)??0; emit(state.copyWith(appThemeMode: mode)); } void switchThemeMode() async { int newMode = state.appThemeMode == 0 ? 1 : 0; emit(state.copyWith(appThemeMode: newMode)); // 存储数据 (await sp).setInt(SpKey.appThemeModel, newMode); } }

另外,这里提供了 initConfig 事件,在程序启动时读取数据,产出记录的状态数据。在 AppConfigBloc 实例化后,触发事件读取配置。

这样就实现了程序退出后,仍可以保持用户配置信息。但现在有个问题,程序在获取配置之前,只能以默认姿态呈现。对于配置信息、资源、数据库的初始化,最好提供一个场合。一般应用,在进入时都会有个静态或加载动画的首屏页。比如腾讯会议、飞书 等,在这里是初始化的最好时机。

2.首屏界面

比如在这里,首屏给出如下的 logo 信息。在资源初始化完成后,进入主页面。另外,有些额外的逻辑处理:比如资源加载过快时,可能导致一闪而过,体验并不是很好。

可以指定最小的首屏等待时间,比如这里 600 ms ,如下是监听初始完成后的逻辑。注意这里跳转时使用 pushReplacement 让闪屏页被替换掉,否则还可以回退。

---->[views/splash/splash_page.dart]---- void _listenInit(BuildContext context, AppConfig state) async{ int now = DateTime.now().millisecondsSinceEpoch; int cost = now - _timeRecoder; int delay = widget.minCostMs - cost; recoder.loadRecord(); if(delay > 0){ await Future.delayed(Duration(milliseconds: delay)); } if(state.inited){ Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_)=>const HomePage())); } }

另外,在等待期间,可以触发 RecordBloc#loadRecord 来预加载首屏信息,这样更有利于应用体验。如果是期望在首屏数据加载完后再进入主页面,可以自己优化一下流程。监听 RecordBloc 的状态改变进入即可。本质上就是在某个契机下执行跳转而已,如果有登陆系统,也是类似。

3. 业务逻辑的处理

这里为 AppConfig 添加一个 inited 的布尔值,用于表示是否已初始化:

在 AppConfigBloc#initApp 中对数据库、配置信息进行读取,产出状态:

---->[blocs/app_config/bloc.dart]---- void initApp() async{ // 读取数据 int mode = (await sp).getInt(SpKey.appThemeModel)??0; await LocalDb.instance.initDb(); emit(state.copyWith(appThemeMode: mode,inited: true)); }

在 Splash 状态类初始化时,触发 AppConfigBloc#initApp 来加载资源,通过 BlocListener 监听初始化的情况:

4.关于业务逻辑状态的监听

另外,要注意一点,在 SplashPage 中,用于主页组件并未挂载在树中,所以主页面中对 Bloc 的监听是无法触发的。如果希望预加载首页内容,需要将主页面的业务逻辑监听提前。

这里定义一个 BlocRelation 组件,专门处理业务逻辑见的关联关系。比如激活记录时,需要处理 匹配面板 和 关联正则 的更新。这些原来是在 HomePage 状态类中处理的,通过这样处理,可以更好地独立维护逻辑,减少 HomePage 状态类中不必要的责任。

四、小细节的优化

最后来优化一点小细节,如下所示,现在期望在输入框中无输入字符时,保存按钮呈灰色,不可点击;有字符时才呈绿色,对输入内容进行存储:

1. 按钮的细节处理

由于可以通过 MatchBloc 知道当前的正则内容,所以很容易想到:使用 select 来观察它,如下通过 emptyRegex 来区分构造逻辑。

class SaveRegexButton extends StatelessWidget { const SaveRegexButton({super.key}); @override Widget build(BuildContext context) { bool emptyRegex = context.select( (value) => value.state.pattern.isEmpty, ); return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8.0), child: GestureDetector( onTap: emptyRegex? null : ()=> _onSaveLinkRegex(context), child: Icon( TolyIcon.save, size: 24, color: emptyRegex?Colors.grey: const Color(0xff59A869), ), ), ); }

像一些依赖状态数据决定是否激活的按钮或其他组件,都可以通过类似的方式来处理。比如面板中的编辑和删除按钮,在数据为空时也可以类似处理,呈灰色无法点击。从这里也能看出状态管理的灵活性:

2. 点击按钮的缩放动画

在移动端,点击时有水波纹来给用户点触的反馈,但对于桌面端来说,水波纹在视觉上并不是太好。在以前我简单封装过一个 FeedbackWidget,如下所示,在点击时可以让组件有缩放的点触回馈。这样在视觉上要好一些,特别是旋转文件时,需要稍作等待才能弹出选框:

使用非常简单,将 GestureDetector 换成 FeedbackWidget,通过 onPressed 回调触发事件即可:

3. 文件选择的优化

当加入记录面板后,文件选择功能就会很尴尬。如下所示,选中后虽然可以修改主页面内容,但是记录激活信息在视觉上就会有冲突:

其实可以在选择后,作为一个新纪录添加到记录面板中,这样逻辑就比较自洽。效果如下:

逻辑处理也并不复杂,和添加记录基本一致,这里为了避免文件过大,只去内容的前 1000 字符录入:

void _onFileSelect(File file) async{ String content = file.readAsStringSync(); if(content.length>1000){ content = content.substring(0,1000); } RecordBloc bloc = context.read(); await bloc.repository.insert(Record.i( title: path.basenameWithoutExtension(file.path), content: content, )); bloc.loadRecord(operation: LoadType.add); } 4. 记录多页签处理

如下所示,现在期望在点击记录,在主内容上方可以展示最近打开的记录,作为页签,支持点击查看、移除操作。效果如下:

这个需求看起来比较复杂,但是本质上就是维护另一份激活数据列表。你把它看成横向排列的记录面板,就很容易理解。其中点击页签条目的行为,和左侧面板也是一致的。那在哪里维护 另一份激活数据列表呢?

这个需求仍属于记录的范畴,可以在 RecordBloc 中进行处理,拓展一下 LoadedRecordState 的数据内容。如下所示,增加 cacheTabs 成员表示点击后缓存的页签:

核心的逻辑处理是在 select 时,维护 cacheRecord 的状态。另外增删改时,也要进行一定的处理。这里数据维护的细节和前面记录数据很类似,这里就不细说了,可以自己查看源码中的 cacheRecord 维护的相关逻辑。

---->[blocs/record/record_bloc.dart]---- void select(int id) { if(state.activeRecord?.id==id) return; if(state is! LoadedRecordState) return; LoadedRecordState _state = state as LoadedRecordState; // 维护 cache tab List cache = _state.cacheRecord; List containsList = cache.where((e) => e.id==id).toList(); if(containsList.isNotEmpty){ //缓存包含激活记录,将记录移到缓存首位 cache.removeWhere((e) => e.id==id); cache.insert(0, containsList.first); }else{ Record record = _state.records.where((e) => e.id==id).first; cache.insert(0, record); } emit(state.copyWith(activeRecordId: id)); }

到这里,本小册中期望构建的应用,在桌面端相关的需求就已经处理完毕,接下来将进入 移动端 界面的适配工作。因为桌面端和移动端屏幕的差异性巨大,这也是跨端应用非常棘手的问题。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有